﻿/*
 * KLF Headless Browser
 * Written by Kris Oye <kristianoye@gmail.com>
 * Copyright (C) 2011-2012.  All rights reserved.
 */
namespace KLF.Lib.Browser.DOM.Parser
{
    #region Using Statements

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text.RegularExpressions;
    using System.Web;
    using System.Xml.Linq;
    using KLF.Lib.Browser.DOM.Parser;
    using KLF.Lib.Browser.DOM.HTML;

    #endregion

    public class DocumentBuilder
    {
        #region Properties

        static readonly string[] SelfClosing = new[]
        {
            "area", 
            "base", 
            "basefont", 
            "br", 
            "hr", 
            "iframe", 
            "input", 
            "img", 
            "link", 
            "meta", 
            "param"
        };

        private readonly List<ParserToken> _tokens;
        private Document _doc;

        #endregion

        #region Constructors

        private DocumentBuilder(List<ParserToken> tokens)
        {
            _tokens = tokens;
            _doc = new HTMLImplementation().createDocument("html",
                "-//W3C//DTD HTML 4.01 Transitional//EN",
                "http://www.w3.org/TR/html4/loose.dtd");
        }

        private DocumentBuilder(List<ParserToken> tokens, Document doc)
        {
            _tokens = tokens;
            _doc = doc;
        }

        #endregion

        #region Methods

        public static Document Parse(List<ParserToken> tokens)
        {
            var hdb = new DocumentBuilder(tokens);
            hdb.Assemble();
            return hdb._doc;
        }

        private void ParseFragment(Node parent)
        {
            var stack = new Stack<Node>();

            Func<Node> topOrRoot = () => stack.Count == 0 ? parent : stack.Peek();

            while (_index < _tokens.Count)
            {
                var token = _tokens[_index++];
                switch (token.Type)
                {
                    case TokenType.Element:
                        {
                            var name = SanitizeElementName(token.A);
                            var current = _doc.createElement(name);
                            topOrRoot().appendChild(current);

                            ReadAttributes(current);

                            if (!SelfClosing.Contains(name))
                                stack.Push(current);
                            break;
                        }

                    case TokenType.CloseElement:
                        {
                            var name = SanitizeElementName(token.A);

                            if (stack.Any(x => ((Element)x).TagName == name))
                                do
                                {
                                    var x = (Element)stack.Pop();
                                    if (x.TagName == name)
                                        break;
                                } while (stack.Count > 0);
                            break;
                        }

                    case TokenType.Comment:
                        topOrRoot().appendChild(_doc.createComment(token.A));
                        break;

                    case TokenType.DocTypeDeclaration:
                        break;

                    case TokenType.Text:
                        topOrRoot().appendChild(_doc.createTextNode(token.A));
                        break;
                }
            }
        }

        public static void ParseFragment(string source, Document doc, Node parent)
        {
            var builder = new DocumentBuilder(Tokenizer.Parse(source), doc);

            builder.ParseFragment(parent);
        }

        public static Document Parse(string source)
        {
            return Parse(Tokenizer.Parse(source));
        }

        private string SanitizeElementName(string name)
        {
            if (name.Contains(":"))
                name = name.Substring(name.LastIndexOf(":") + 1);
            return name.ToLowerInvariant();
        }

        int _index;
        private void Assemble()
        {
            var stack = new Stack<Node>();

            Func<Node> topOrRoot = () => stack.Count == 0 ? _doc : stack.Peek();
            _doc.__state = DocumentState.Loading;

            while (_index < _tokens.Count)
            {
                var token = _tokens[_index++];
                switch (token.Type)
                {
                    case TokenType.Element:
                        {
                            var name = SanitizeElementName(token.A);
                            var current = _doc.createElement(name);
                            topOrRoot().appendChild(current);

                            ReadAttributes(current);

                            if (!SelfClosing.Contains(name))
                                stack.Push(current);
                            break;
                        }

                    case TokenType.CloseElement:
                        {
                            var name = SanitizeElementName(token.A);

                            if (stack.Any(x => ((Element)x).TagName == name))
                                do
                                {
                                    var x = (Element) stack.Pop();
                                    if (x.TagName == name)
                                        break;
                                } while (stack.Count > 0);
                            break;
                        }

                    case TokenType.Comment:
                        topOrRoot().appendChild(_doc.createComment(token.A));
                        break;

                    case TokenType.DocTypeDeclaration:
                        _doc = new DocumentType(_doc, token.Raw).CreateDocument();
                        break;

                    case TokenType.Text:
                        topOrRoot().appendChild(_doc.createTextNode(token.A));
                        break;
                }
            }
            _doc.__state = DocumentState.Interactive;
        }

        static readonly Regex RxValidAttrName = new Regex(@"^[A-Za-z_][A-Za-z0-9_\-\.]*$");

        private void ReadAttributes(Node current)
        {
            while (_index < _tokens.Count && _tokens[_index].Type == TokenType.Attribute)
            {
                var token = _tokens[_index++];
                var name = token.A.ToLowerInvariant();

                if (name != "xmlns" && RxValidAttrName.IsMatch(name))
                {
                    Element e = current as Element;

                    if (e != null)
                    {
                        string val = HttpUtility.HtmlDecode(token.B ?? token.A ?? "");

                        e.setAttribute(name, val);
                    }
                }
            }
        }

        #endregion

        internal static Node ParseFragment(string sText, HTMLDocument Owner)
        {
            throw new NotImplementedException();
        }
    }
}

